joe di castrohttp://joedicastro.com2011-04-28T14:04:00+02:00Comprobar si un programa está instalado con Python2011-04-28T14:04:00+02:00joe di castrohttp://joedicastro.com/comprobar-si-un-programa-esta-instalado-con-python.html<p>Cuando creamos un script en <strong>Python</strong>, sobre todo aquellos orientados a ejecutarse en la línea de comandos, a veces necesitamos echar mano de un programa externo que no siempre viene por defecto instalado en el sistema. Por ejemplo, en el script que empleo en <a href="http://joedicastro.com/optimizar_imagenes_para_la_web">optimizar imágenes para la web</a> empleo los programas externos <em>pngcrush</em> y <em>jpegtran</em>. ¿Como comprobamos entonces si el programa está instalado? Desde luego es siempre mejor comprobarlo y avisar al usuario, que dejar que arroje un feo error.</p> <p>Una forma de comprobar si el programa está instalado es capturando la excepción cuando se produzca con las sentencias <code>try</code> y <code>except</code>, incluyendo la llamada al ejecutable dentro de ellas y devolver un mensaje de error avisando de la necesidad de la presencia de este ejecutable. Pero al igual que en el ejemplo anterior, yo prefiero comprobar esto incluso antes de ejecutar cualquier otro código dentro del script. Para ello empleo el <a href="http://ibiblio.org/g2swap/byteofpython/read/module-name.html">conocido truco</a> que nos permite ejecutar cierto código solo cuando es ejecutado como script y no cuando es importado como módulo:</p> <div class="codehilite"><pre><span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">&quot;__main__&quot;</span><span class="p">:</span> <span class="n">chequeo_ejecutable</span><span class="p">()</span> <span class="n">main</span><span class="p">()</span> </pre></div> <p>Donde <code>main()</code> es la función principal donde albergaríamos el código fundamental del script. Es un conocido <a href="http://python.net/~goodger/projects/pycon/2007/idiomatic/handout.html">Python Idiom</a> que me sirve perfectamente para esta tarea. De hecho lo empleo habitualmente en todos mis scripts para separar el código principal de las funciones. </p> <p>Comprobar en Linux si existe el ejecutable es realmente sencillo (siempre y cuando conozcamos el nombre exacto del ejecutable) con este código:</p> <div class="codehilite"><pre><span class="c">#!/usr/bin/env python</span> <span class="c"># -*- coding: utf8 -*-</span> <span class="kn">import</span> <span class="nn">sys</span> <span class="kn">from</span> <span class="nn">subprocess</span> <span class="kn">import</span> <span class="n">Popen</span><span class="p">,</span> <span class="n">PIPE</span> <span class="k">def</span> <span class="nf">check_execs</span><span class="p">(</span><span class="o">*</span><span class="n">progs</span><span class="p">):</span> <span class="sd">&quot;&quot;&quot;Check if the programs are installed, if not exit and report.&quot;&quot;&quot;</span> <span class="k">for</span> <span class="n">prog</span> <span class="ow">in</span> <span class="n">progs</span><span class="p">:</span> <span class="k">try</span><span class="p">:</span> <span class="n">Popen</span><span class="p">([</span><span class="n">prog</span><span class="p">,</span> <span class="s">&#39;--help&#39;</span><span class="p">],</span> <span class="n">stdout</span><span class="o">=</span><span class="n">PIPE</span><span class="p">,</span> <span class="n">stderr</span><span class="o">=</span><span class="n">PIPE</span><span class="p">)</span> <span class="k">except</span> <span class="ne">OSError</span><span class="p">:</span> <span class="n">msg</span> <span class="o">=</span> <span class="s">&#39;The {0} program is necessary to run this script&#39;</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">prog</span><span class="p">)</span> <span class="n">sys</span><span class="o">.</span><span class="n">exit</span><span class="p">(</span><span class="n">msg</span><span class="p">)</span> <span class="k">return</span> <span class="k">def</span> <span class="nf">main</span><span class="p">():</span> <span class="c"># Incluir aquí el código fundamental del script</span> <span class="k">pass</span> <span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">&quot;__main__&quot;</span><span class="p">:</span> <span class="n">check_execs</span><span class="p">(</span><span class="s">&#39;python&#39;</span><span class="p">)</span> <span class="n">main</span><span class="p">()</span> </pre></div> <p>Si no existiera el ejecutable que le pasamos a la función <code>check_execs()</code> entonces se detendrá el script y nos dirá que necesitamos instalarlo para ejecutar el script. Hacerlo así nos evita cualquier tipo de manipulación previa antes de darnos cuenta de que nos falta un elemento esencial para ejecutarlo al completo. Esta función funciona porque en Linux los ejecutables normalmente están colgando de una ruta del PATH. En Windows, por ejemplo, la cosa cambia.</p> <p>Si queremos que el script sea multiplataforma y que nos funcione tanto en sistemas *NIX como en Windows entonces necesitamos una función algo más compleja. En Windows un ejecutable no necesita colgar del PATH y es muy frecuente que no sea así. Al mismo tiempo en Windows podemos tener el sistema de ficheros repartido en varias unidades de disco, y necesitamos explorar todas ellas para buscar nuestro ejecutable. Aunque lo más frecuente es que se encuentre en la unidad C: y la función se detiene en cuanto encuentra el primer ejecutable, el hecho de realizar está búsqueda hace que el proceso sea más lento en Windows que en Linux. </p> <p>La función multiplataforma es la siguiente:</p> <div class="codehilite"><pre><span class="k">def</span> <span class="nf">check_execs_posix_win</span><span class="p">(</span><span class="n">progs</span><span class="p">):</span> <span class="sd">&quot;&quot;&quot;Check if the program is installed.</span> <span class="sd"> Returns one dictionary with 1+n pair of key/values:</span> <span class="sd"> A fixed key/value:</span> <span class="sd"> &quot;WinOS&quot; -- (boolean) True it&#39;s a Windows OS, False it&#39;s a *nix OS</span> <span class="sd"> for each program in progs a key/value like this:</span> <span class="sd"> &quot;program&quot; -- (str or boolean) The Windows executable path if founded else </span> <span class="sd"> &#39;&#39; if it&#39;s Windows OS. If it&#39;s a *NIX OS True</span> <span class="sd"> if founded else False</span> <span class="sd"> &quot;&quot;&quot;</span> <span class="n">execs</span> <span class="o">=</span> <span class="p">{</span><span class="s">&#39;WinOS&#39;</span><span class="p">:</span><span class="bp">True</span> <span class="k">if</span> <span class="n">platform</span><span class="o">.</span><span class="n">system</span><span class="p">()</span> <span class="o">==</span> <span class="s">&#39;Windows&#39;</span> <span class="k">else</span> <span class="bp">False</span><span class="p">}</span> <span class="c"># get all the drive unit letters if the OS is Windows</span> <span class="n">windows_drives</span> <span class="o">=</span> <span class="n">findall</span><span class="p">(</span><span class="s">r&#39;(\w:)</span><span class="se">\\</span><span class="s">&#39;</span><span class="p">,</span> <span class="n">Popen</span><span class="p">(</span><span class="s">&#39;fsutil fsinfo drives&#39;</span><span class="p">,</span> <span class="n">stdout</span><span class="o">=</span><span class="n">PIPE</span><span class="p">)</span><span class="o">.</span> <span class="n">communicate</span><span class="p">()[</span><span class="mi">0</span><span class="p">])</span> <span class="k">if</span> <span class="n">execs</span><span class="p">[</span><span class="s">&#39;WinOS&#39;</span><span class="p">]</span> <span class="k">else</span> <span class="bp">None</span> <span class="n">progs</span> <span class="o">=</span> <span class="p">[</span><span class="n">progs</span><span class="p">]</span> <span class="k">if</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">progs</span><span class="p">,</span> <span class="nb">str</span><span class="p">)</span> <span class="k">else</span> <span class="n">progs</span> <span class="k">for</span> <span class="n">prog</span> <span class="ow">in</span> <span class="n">progs</span><span class="p">:</span> <span class="k">if</span> <span class="n">execs</span><span class="p">[</span><span class="s">&#39;WinOS&#39;</span><span class="p">]:</span> <span class="c"># Set all commands to search the executable in all drives</span> <span class="n">win_cmds</span> <span class="o">=</span> <span class="p">[</span><span class="s">&#39;dir /B /S {0}\*{1}.exe&#39;</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">letter</span><span class="p">,</span> <span class="n">prog</span><span class="p">)</span> <span class="k">for</span> <span class="n">letter</span> <span class="ow">in</span> <span class="n">windows_drives</span><span class="p">]</span> <span class="c"># Get the first location (usually in C:) where the executable exists</span> <span class="k">for</span> <span class="n">cmd</span> <span class="ow">in</span> <span class="n">win_cmds</span><span class="p">:</span> <span class="n">execs</span><span class="p">[</span><span class="n">prog</span><span class="p">]</span> <span class="o">=</span> <span class="p">(</span><span class="n">Popen</span><span class="p">(</span><span class="n">cmd</span><span class="p">,</span> <span class="n">stdout</span><span class="o">=</span><span class="n">PIPE</span><span class="p">,</span> <span class="n">stderr</span><span class="o">=</span><span class="n">PIPE</span><span class="p">,</span> <span class="n">shell</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span><span class="o">.</span> <span class="n">communicate</span><span class="p">()[</span><span class="mi">0</span><span class="p">]</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="n">os</span><span class="o">.</span><span class="n">linesep</span><span class="p">)[</span><span class="mi">0</span><span class="p">])</span> <span class="k">if</span> <span class="n">execs</span><span class="p">[</span><span class="n">prog</span><span class="p">]:</span> <span class="k">break</span> <span class="k">else</span><span class="p">:</span> <span class="k">try</span><span class="p">:</span> <span class="n">Popen</span><span class="p">([</span><span class="n">prog</span><span class="p">,</span> <span class="s">&#39;--help&#39;</span><span class="p">],</span> <span class="n">stdout</span><span class="o">=</span><span class="n">PIPE</span><span class="p">,</span> <span class="n">stderr</span><span class="o">=</span><span class="n">PIPE</span><span class="p">)</span> <span class="n">execs</span><span class="p">[</span><span class="n">prog</span><span class="p">]</span> <span class="o">=</span> <span class="bp">True</span> <span class="k">except</span> <span class="ne">OSError</span><span class="p">:</span> <span class="n">execs</span><span class="p">[</span><span class="n">prog</span><span class="p">]</span> <span class="o">=</span> <span class="bp">False</span> <span class="k">return</span> <span class="n">execs</span> </pre></div> <p>En la parte de Windows (la de *NIX es básicamente igual) esta función lo que hace es obtener primero las letras de las unidades de disco disponibles en el sistema. Luego busca el ejecutable en cada una de ellas y en cuanto encuentra la primera ruta al ejecutable se detiene. La función en este caso devuelve un diccionario donde hay una clave fija que es <code>WinOS</code> que será <code>True</code> si estamos en Windows y <code>False</code> en caso contrario. Luego nos devuelve una clave por cada uno de los programas que le mandemos comprobar. Esta clave sera <abbr title="Verdadero o Falso. El nombre proviene de la Álgebra de Boole.">booleana</abbr> en el caso de *NIX y la ruta del programa (o una cadena vacía) en el caso de Windows.</p> <p>Las funciones y un ejemplo de su funcionamiento podéis encontrarlas en el fichero <code>check_execs.py</code> en mi repositorio <em>Python Recipes</em> que se encuentra alojado en <a href="http://github.com/joedicastro/python-recipes">github</a>.</p>